// TODO: CODE REVIEW & CLEANUP!
//
// HttpServer.cs
//
// Implements HttpServer and related types.
//

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using HomeUX.Utilities;
using Microsoft.Win32;
using System.Threading;

namespace HomeUX.Server
{

public class HttpServer
{
    int _portNumber = 80;
    string _httpPath;
    string _localPath;

    HttpListener _httpListener;

    object _httpListenerLock = new object();

    public int PortNumber
    {
        get
        {
            return _portNumber;
        }
        set
        {
            DisallowPropertySetAfterStart("PortNumber");
            _portNumber = value;
        }
    }

    public string HttpPath
    {
        get
        {
            return _httpPath;
        }
        set
        {
            DisallowPropertySetAfterStart("HttpPath");
            _httpPath = value;
        }
    }

    public string LocalPath
    {
        get
        {
            return _localPath;
        }
        set
        {
            DisallowPropertySetAfterStart("LocalPath");
            _localPath = value;
        }
    }

    /// <summary>
    /// Begins accepting incoming connections.
    /// </summary>
    /// 
    /// <exception cref="FatalException">
    /// The <r>HttpServer</r> could not be started.
    /// </exception>
    ///
    public void Start()
	{
        if (_httpPath == null)
            RequirePropertySetBeforeStart("HttpPath");
        if (_localPath == null)
            RequirePropertySetBeforeStart("LocalPath");

        try
        {
            lock (_httpListenerLock)
            {
                _httpListener = new HttpListener();
                _httpListener.Prefixes.Add(String.Format("http://*:{0}/{1}/", _portNumber, _httpPath));
                _httpListener.Start();
                _httpListener.BeginGetContext(HttpRequestReceived, null);
            }
        }
        catch (Exception ex)
        {
            if ((ex is PlatformNotSupportedException) || (ex is HttpListenerException) ||
                (ex is ArgumentException))
                throw new FatalException(ex, "HttpServer failed to listen on port {0}", _portNumber);
            else
                throw;
        }
	}

    public void Stop()
    {
		lock (_httpListenerLock)
		{
			if (_httpListener != null)
			{
				_httpListener.Stop();
				_httpListener = null;
			}
		}
    }

    public void HttpRequestReceived(IAsyncResult ar)
    {
        lock (_httpListenerLock)
        {
            // do nothing if <_httpListener> was stopped
            if (_httpListener == null)
                return;

            // process this request
            HttpListenerContext context = _httpListener.EndGetContext(ar);
            if (ProcessHttpRequest(context.Request, context.Response))
            {
                // rarely, I get an InvalidOperationException, "cannot close stream
                // until writing is complete"
                try
                {
                    context.Response.OutputStream.Close();
                }
                catch (InvalidOperationException)
                {
                    context.Response.Abort();
                }
            }
            else
                context.Response.Abort();

            // listen for the next request
            _httpListener.BeginGetContext(HttpRequestReceived, null);
        }
    }

	bool ProcessHttpRequest(HttpListenerRequest request, HttpListenerResponse response)
	{
        // set <path> to the relative path of the file being requested; reject paths to files
        // that aren't within the client directory
        string[] segments = request.Url.Segments;
        if ((segments.Length < 2) || (segments[0] != "/"))
            return false;
        StringBuilder pathBuilder = new StringBuilder(100);
        if ((segments.Length == 2) && (segments[0] == "/"))
        {
            string segment = segments[1];
            if (segment.EndsWith("/"))
                pathBuilder.Append(@"\Default.htm");
            else
            {
                response.Redirect(request.Url.OriginalString + "/");
                return true;
            }
        }
        else
        {
            char[] invalidChars = Path.GetInvalidFileNameChars();
            for (int index = 2; index < segments.Length; index++)
            {
                string segment = segments[index];
                if (segment.EndsWith("/"))
                    segment = segment.Substring(0, segment.Length - 1);
                if ((segment.Length == 0) || (segment.IndexOfAny(invalidChars) >= 0) ||
                        segment.Contains(".."))
                    return false;
                pathBuilder.Append('\\');
                pathBuilder.Append(segment);
            }
        }
        string path = pathBuilder.ToString();

        bool isTouchScreen;
        if (path.Equals(@"\TouchScreen", StringComparison.InvariantCultureIgnoreCase))
        {
            isTouchScreen = true;
            path += ".htm";
        }
        else
            isTouchScreen = false;

        // set <buffer> to the data to return, and set <contentType> to its content type
        byte[] buffer;
        string contentType;
        if (path.Equals(@"\Time", StringComparison.InvariantCultureIgnoreCase))
        {
            // set <buffer> HTML containing the current time
            buffer = Encoding.UTF8.GetBytes(String.Format(
                "<html><body>The current time is: <b>{0}</b></body></html>", DateTime.Now));
            contentType = "text/html";
        }
        else
        {
            // see if <path> refers to a static file within the client directory by changing <path>
            // to the full path to the static file
            string fullPath = _localPath + path;
            if (!File.Exists(fullPath))
            {
                fullPath = Path.Combine(_localPath, @"bin\Debug") + path;
                if (!File.Exists(fullPath))
                {
                    fullPath = Path.Combine(_localPath, @"bin\Release") + path;
                    if (!File.Exists(fullPath))
                    {
                        // note that File.Exists is presumably faster than throwing and catching an
                        // exception -- which is helpful in case of denial of service attacks
                        return false;
                    }
                }
            }

            // load <path> into <buffer>
            buffer = File.ReadAllBytes(fullPath);

            // if the client is requesting the touch screen HTML file, update placeholders such as
            // "%ServerHostName%" within <buffer>
            if (isTouchScreen)
            {
                string fileContents = Encoding.UTF8.GetString(buffer);

                bool isHideCursorMode = GetBooleanQueryString(request, "IsHideCursorMode", false);

                fileContents = fileContents
                    .Replace("%IsHideCursorMode%", (isHideCursorMode ? "true" : "false"))
                    .Replace("%ServerHostName%", request.Url.Authority);

                buffer = Encoding.UTF8.GetBytes(fileContents);
            }

            // set <contentType> to the content type associated with this file
            // TODO: cache this
            string extension = Path.GetExtension(fullPath);
            using (RegistryKey key = Registry.ClassesRoot.OpenSubKey(extension))
            {
                if ((key == null) ||
                    ((contentType = key.GetValue("Content Type").ToString()) == null))
                    contentType = "application/octet-stream";
            }
        }

        response.ContentLength64 = buffer.Length;
        response.ContentType = contentType;
        response.AddHeader("Pragma", "no-cache");
        response.AddHeader("Expires", "-1");
        try
        {
            response.OutputStream.Write(buffer, 0, buffer.Length);
        }
        catch (HttpListenerException)
        {
            // ignore HttpListenerException, e.g. client disconnected prematurely
        }
        return true;
	}

    /// <summary>
    /// Returns the <n>Boolean</n> value of a query string item, or a default value of the item
    /// isn't found.
    /// </summary>
    ///
    /// <param name="request">The <n>HttpListenerRequest</n> containing the query string.</param>
    ///
    /// <param name="name">The name of the query string item to look for.</param>
    ///
    /// <param name="defaultValue">The value to return if the query string item isn't found or
    ///     can't be converted to type <n>Boolean</n>.</param>
    ///
    static Boolean GetBooleanQueryString(HttpListenerRequest request, string name, bool defaultValue)
    {
        string stringValue = request.QueryString[name];
        if (stringValue != null)
        {
            try
            {
                return XmlConvert.ToBoolean(stringValue);
            }
            catch (FormatException)
            {
                return defaultValue;
            }
        }
        else
            return defaultValue;
    }

	void RequirePropertySetBeforeStart(string propertyName)
	{
		throw new InvalidOperationException(String.Format(
			"Property \"{0}\" must be set before Start has been called",
			propertyName));
	}

    void DisallowPropertySetAfterStart(string propertyName)
    {
        if (_httpListener != null)
        {
            throw new InvalidOperationException(String.Format(
                "Property \"{0}\" may only be set before Start has been called",
                propertyName));
        }
    }
}

}
